package com.spring.nio.core.service.upload;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.spring.nio.common.BasicConstant;
import com.spring.nio.common.ResponseEnum;
import com.spring.nio.core.context.NioServerContext;
import com.spring.nio.core.service.frame.FileFrame;
import com.spring.nio.core.service.model.FileTask;
import com.spring.nio.core.service.model.request.FileUploadRequest;
import com.spring.nio.core.service.model.response.FileUploadResponse;
import com.spring.nio.util.BasicUtil;
import com.spring.nio.util.LocalTime;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * @author: YSFY
 * @date: 2021-04-18 18:35
 * @pacage_name: com.spring.nio.core.service.upload
 * @class_name: FileUploadRunnable
 * @description: File 上传
 */

@Slf4j
public class FileUploadRunnable implements Runnable {
    private static final String COMMON_USER = "spring";
    private FileTask fileTask;
    public FileUploadRunnable(FileTask fileTask) {
        this.fileTask = fileTask;
    }

    @Override
    public void run() {
        log.info("[" + LocalTime.formatDate(LocalDateTime.now()) + "] FileService | --> 文件名 = [{}] 开始上传, thread = [{}]", this.fileTask.getFile().getName(), Thread.currentThread().getName());
        this.sendUploadBuffer(this.createSendBuffer(this.createFileUploadFrame(this.createFileUploadRequest())));
    }

    /**
     * 创建文件上传请求
     * @return fileUploadRequest
     * */
    private FileUploadRequest createFileUploadRequest() {
        FileUploadRequest request = new FileUploadRequest();
        request.setLaunchUserName(COMMON_USER);
        request.setFileName(this.fileTask.getFile().getName());
        request.setFileSize(this.fileTask.getFile().length());
        request.setFilePath(this.fileTask.getRequestData().getServerRestorePath());
        request.setTag(UUID.randomUUID().toString());
        request.setNotLogin(Boolean.TRUE);
        return request;
    }

    /**
     * 创建文件传输帧
     * @param request
     * @return fileFrame
     * */
    private FileFrame createFileUploadFrame(FileUploadRequest request) {
        FileFrame fileFrame = new FileFrame();
        byte[] requestDataBytes = JSON.toJSONString(request).getBytes(Charset.forName("utf-8"));
        fileFrame.setFrameSumLength(FileFrame.FRAME_COMMON_LENGTH + requestDataBytes.length);
        fileFrame.setIsEnd((byte) 1);
        fileFrame.setFrameIndex(1);
        fileFrame.setFrameType(FileFrame.FrameType.valueOf(this.fileTask.getRequestData().getExec()));
        fileFrame.setFileType(FileFrame.FileType.ALL);
        fileFrame.setFileOperateType(FileFrame.FileOperateType.STORE);
        fileFrame.setFileLength(request.getFileSize());
        fileFrame.setRequest(requestDataBytes);
        return fileFrame;
    }

    /**
     * 文件帧构建文件发送字节缓冲区
     * @param fileUploadFrame
     * @return byteBuffer
     * */
    private ByteBuffer createSendBuffer(FileFrame fileUploadFrame) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(fileUploadFrame.getFrameSumLength());
        // 帧总长度 4B
        byteBuffer.put(BasicUtil.intToBytes(fileUploadFrame.getFrameSumLength()));
        // 是否是结束帧 1B
        byteBuffer.put(fileUploadFrame.getIsEnd());
        // 帧序号 4B
        byteBuffer.put(BasicUtil.intToBytes(fileUploadFrame.getFrameIndex()));
        // 帧类型 1B
        byteBuffer.put(((byte) Integer.valueOf(fileUploadFrame.getFrameType().getBit(), 2).intValue()));
        // 文件类型 1B
        byteBuffer.put(((byte) Integer.valueOf(fileUploadFrame.getFileType().getBit(), 2).intValue()));
        // 文件操作类型 1B
        byteBuffer.put(((byte) Integer.valueOf(fileUploadFrame.getFileOperateType().getBit(), 2).intValue()));
        // 文件数据长度 2B
        byteBuffer.put(BasicUtil.unsignedShortToByte2(fileUploadFrame.getRequest().length));
        // 文件大小长度 8B
        byteBuffer.put(BasicUtil.longToBytes(fileUploadFrame.getFileLength()));
        // 文件数据内容 ?
        byteBuffer.put(fileUploadFrame.getRequest());
        return byteBuffer;
    }

    /**
     * 发送文件数据
     * @param byteBuffer
     * */
    private void sendUploadBuffer(ByteBuffer byteBuffer) {
        SocketChannel socketChannel = null;
        try {
            // 1、建立连接
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(Boolean.FALSE);
            InetSocketAddress inetSocketAddress = new InetSocketAddress(InetAddress.getByName(NioServerContext.getValue(BasicConstant.SERVER_IP)), Integer.parseInt(NioServerContext.getValue(BasicConstant.NIO_FILE_UPLOAD_PORT)));
            socketChannel.connect(inetSocketAddress);
            while (!socketChannel.isConnected()) {
                if(socketChannel.finishConnect()) {
                    break;
                }
            }

            // 2、发送文件数据
            byteBuffer.flip();
            socketChannel.write(byteBuffer);

            // 3、读取文件上传反馈数据并校验，校验成功后开始发送文件数据
            this.readDataAndSend(socketChannel);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(null != socketChannel && socketChannel.isOpen()) {
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 读取文件上传反馈数据并校验，校验成功后开始发送文件数据
     * @param socketChannel
     * */
    private void readDataAndSend(SocketChannel socketChannel) throws IOException {
        int readBytes = 0;
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        List<byte[]> byteList = Lists.newArrayList();
        while(true) {
            readBytes = socketChannel.read(byteBuffer);
            if(readBytes > 0) {
                byteBuffer.flip();
                byte[] bytes = new byte[readBytes];
                while (byteBuffer.hasRemaining()) {
                    byteBuffer.get(bytes, 0, bytes.length);
                    byteList.add(bytes);
                }
                byteBuffer.clear();
                continue;
            }

            break;
        }

        if(!CollectionUtils.isEmpty(byteList)) {
            // 合并数组
            int sumLength = 0;
            for(byte[] bytes : byteList) {
                sumLength += bytes.length;
            }

            byte[] sumDataBytes = new byte[sumLength];
            int copyIndex = 0;
            for(byte[] bytes : byteList) {
                System.arraycopy(bytes, 0, sumDataBytes, copyIndex, bytes.length);
                copyIndex += bytes.length;
            }

            // 如果解析成功，执行文件流传输
            StringBuffer stringBuffer = new StringBuffer();
            Boolean parseResult = this.parseFileBytes(sumDataBytes, stringBuffer);
            if(parseResult) {
                // 文件上传响应校验
                FileUploadResponse response = JSON.parseObject(stringBuffer.toString(), FileUploadResponse.class);
                log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileUploadRunnable | --> 文件 [{}] 上传任务校验结果, code = [{}], message = [{}], thread = {}", this.fileTask.getFile().getName(), response.getCode(), response.getMessage(), Thread.currentThread().getName());
                if(!StringUtils.equals(ResponseEnum.SUCCESS.getCode(), response.getCode())) {
                    return;
                }

                // 文件开始执行上传
                this.sendFileStream(response, socketChannel);
            }
        }

    }

    /**
     * 解析文件上传响应结果
     * @param bytes
     * @param stringBuffer
     * @return
     * */
    private Boolean parseFileBytes(byte[] bytes, StringBuffer stringBuffer) {
        Boolean result = Boolean.TRUE;

        // 解析文件上传响应帧总长度
        byte[] sumLengthBytes = new byte[4];
        sumLengthBytes[0] = bytes[0];
        sumLengthBytes[1] = bytes[1];
        sumLengthBytes[2] = bytes[2];
        sumLengthBytes[3] = bytes[3];
        int sumLength = BasicUtil.byteArrayToInt(sumLengthBytes);
        result = (sumLength == bytes.length)?Boolean.TRUE:Boolean.FALSE;

        // 解析帧类型
        FileFrame.FrameType frameType = Arrays.stream(FileFrame.FrameType.values()).filter(type -> type.getBit().contains(Integer.toBinaryString(bytes[4]))).collect(Collectors.toList()).get(0);

        // 是否是结束帧
        byte endFrame = bytes[5];

        // 解析数据长度
        byte[] dataLengthBytes = new byte[4];
        dataLengthBytes[0] = bytes[6];
        dataLengthBytes[1] = bytes[7];
        dataLengthBytes[2] = bytes[8];
        dataLengthBytes[3] = bytes[9];
        int dataLength = BasicUtil.byteArrayToInt(dataLengthBytes);

        // 解析数据内容
        stringBuffer.append(new String(bytes, 10, dataLength, Charset.forName("utf-8")));
        System.out.println("解析数据内容为: " + stringBuffer.toString());

        return result;
    }

    /**
     * 开始传输文件流
     * @param fileUploadResponse
     * @param socketChannel
     * */
    private void sendFileStream(FileUploadResponse fileUploadResponse, SocketChannel socketChannel) throws IOException {
        // 获取文件通道
        FileChannel fileChannel = FileChannel.open(this.fileTask.getFile().toPath(), StandardOpenOption.READ);
        if(!fileChannel.isOpen()) {
            log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileUploadRunnable | --> 文件 [{}] 通道打开失败, thread = {}", this.fileTask.getFile().getName(), Thread.currentThread().getName());
            return;
        }

        // 封装标准数据
        String flagData = COMMON_USER + "," + fileUploadResponse.getTag();
        byte[] commonDataBytes = flagData.getBytes(Charset.forName("utf-8"));
        byte[] commonDataLengthBytes = BasicUtil.unsignedShortToByte2(commonDataBytes.length);
        Integer standardLength = 4 + 1 + 4 + 3 + commonDataLengthBytes.length + commonDataBytes.length;

        // 发送文件数据
        ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.parseInt(NioServerContext.getValue(BasicConstant.SOCKET_SEND_BUFFER_SIZE)));
        int loopReadCount = 0, alreadyReadCount = 0, packetIndex = 1;
        while ((loopReadCount = fileChannel.read(byteBuffer)) > 0) {
            alreadyReadCount += loopReadCount;

            // 读取文件数据
            byteBuffer.flip();
            byte[] fileStreamBytes = new byte[loopReadCount];
            while (byteBuffer.hasRemaining()) {
                byteBuffer.get(fileStreamBytes, 0, fileStreamBytes.length);
            }
            byteBuffer.clear();

            // 定义文件发送数据字节数组
            byte[] sendBytes = new byte[standardLength + loopReadCount];

            // 帧总长度 4B (0~3)
            byte[] frameSumLengthBytes = BasicUtil.intToBytes(sendBytes.length);
            System.arraycopy(frameSumLengthBytes, 0, sendBytes, 0, frameSumLengthBytes.length);

            // 是否结束帧 1B (4)
            sendBytes[4] = (fileChannel.size() == alreadyReadCount ? (byte) 1 : (byte) 0);

            // 帧序号 4B
            byte[] frameIndexBytes = BasicUtil.intToBytes(packetIndex);
            System.arraycopy(frameIndexBytes, 0, sendBytes, 5, frameIndexBytes.length);

            // 帧类型
            sendBytes[9] = Byte.valueOf(FileFrame.FrameType.DATA_TRANSPORT.getBit());

            // 文件类型
            sendBytes[10] = ((byte) Integer.parseInt(FileFrame.FileType.ALL.getBit(), 2));

            // 文件操作类型
            sendBytes[11] = Byte.valueOf(FileFrame.FileOperateType.TRANSPORT.getBit());

            // 公共文件流数据长度及内容
            System.arraycopy(commonDataLengthBytes, 0, sendBytes, 12, commonDataLengthBytes.length);
            System.arraycopy(commonDataBytes, 0, sendBytes, (12 + commonDataLengthBytes.length), commonDataBytes.length);

            // 文件流数据
            System.arraycopy(fileStreamBytes, 0, sendBytes, (12 + commonDataLengthBytes.length + commonDataBytes.length), fileStreamBytes.length);

            ByteBuffer sendByteBuffer = ByteBuffer.allocate(sendBytes.length);
            sendByteBuffer.put(sendBytes);
            sendByteBuffer.flip();
            int writeBytesLength = socketChannel.write(sendByteBuffer);
            if(writeBytesLength > 0) {
                log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileUploadRunnable | --> 文件 [{}] 流数据上传成功，已发送大小 [{}], 帧序号 [{}], thread = {}", this.fileTask.getFile().getName(), alreadyReadCount, packetIndex, Thread.currentThread().getName());
                // 帧序号加1
                packetIndex++;
            }
        }

        log.info("[ " + LocalTime.formatDate(LocalDateTime.now()) + " ] FileUploadRunnable | --> 文件 [{}] 流数据发送成功，已发送大小 [{}], thread = {}", this.fileTask.getFile().getName(), alreadyReadCount, Thread.currentThread().getName());
    }

}
